//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public import SWBCore
package import SWBUtil

/// Abstract base class for typical task producers.
open class StandardTaskProducer {
    /// Immutable data available to the task producer.
    package let context: TaskProducerContext

    /// Additional paths which invalidate the build description
    package var invalidationPaths: [Path] { return Array(accessedPaths) }

    private var accessedPaths = Set<Path>()

    /// Adds the accessed path to the list of paths which invalidate the build description.
    func access(path: Path) {
        accessedPaths.insert(path)
    }

    /// Reads the contents of the file at `path` and returns its contents as a byte string, and adds the path to the list of paths which invalidate the build description.
    func readFileContents(_ path: Path) throws -> ByteString {
        access(path: path)
        return try context.workspaceContext.fs.read(path)
    }

    func fileExists(at path: Path) -> Bool {
        access(path: path)
        return context.workspaceContext.fs.exists(path)
    }

    func recordAttachment(contents: ByteString) -> Path {
        return context.recordAttachment(contents: contents)
    }

    /// Create a standard task producer.
    /// Should be marked package, but that causes the compiler to generate an unused, infinitely recursive thunk.
    public init(_ context: TaskProducerContext) {
        self.context = context
    }

    /// The default set of `TaskOrderingOptions` for this task producer.  Subclasses should override this to define how tasks they create should be ordered.
    open var defaultTaskOrderingOptions: TaskOrderingOptions {
        return []
    }

    // FIXME: It would be nice to avoid the return argument here, it is very rarely used (just by the clients that do reprocessing).
    //
    /// Convenience function for appending tasks using from a spec.
    ///
    /// By encouraging clients to go through this function for task generation based tasks, we also ensure that they will add their tasks somewhere and not forget to append them to the result.
    @discardableResult
    package func appendGeneratedTasks(_ tasks: inout [any PlannedTask], options: TaskOrderingOptions? = nil, body: (any TaskGenerationDelegate) async -> Void) async -> (tasks: [any PlannedTask], outputs: [FileToBuild]) {
        let options = options ?? defaultTaskOrderingOptions

        let delegate = ProducerBasedTaskGenerationDelegate(producer: self, context: context, taskOptions: options)
        await body(delegate)
        tasks.append(contentsOf: delegate.tasks)
        return (tasks: delegate.tasks, outputs: delegate.outputs )
    }

    func diagnosticFilenameMapContents() async throws -> ByteString {
        // Mappings from active targets override those from inactive targets
        var activeMappings: [String: String] = [:]
        var otherMappings: [String: String] = [:]
        await context.forEachBuildProductToSourceMapping(mapToTempFilesOnly: false) { target, isActiveTarget, builtFile, originalSourceFile in
            if isActiveTarget {
                activeMappings[builtFile.str] = originalSourceFile.str
            } else {
                otherMappings[builtFile.str] = originalSourceFile.str
            }
        }
        let topLevel: [String: PropertyListItem] = ["version": "0".propertyListItem, "case-sensitive": "false".propertyListItem, "contents": otherMappings.addingContents(of: activeMappings).propertyListItem]
        return try topLevel.propertyListItem.asJSONFragment()
    }
}


/// Abstract base class for phased task producers.  All tasks generated by such a producer will be grouped between two nodes so they will all run after tasks before the start node, and before tasks after the end node.  It is the responsibility of the creator of these producers to create them with nodes which reflect the desired ordering.
open class PhasedTaskProducer: StandardTaskProducer {
    /// The phase start nodes.
    var phaseStartNodes: [any PlannedNode]

    /// The phase end node.
    var phaseEndNode: any PlannedNode

    /// The phase end task.  This is created implicitly when the task producer is created, and takes the phase start node as an input and the phase end node as an output.
    var phaseEndTask: any PlannedTask

    /// The list of additional gate tasks which were produced.
    ///
    /// The subclass *must* manually report these, if `addTaskBarrier` is used.
    var additionalGateTasks: [any PlannedTask] = []

    public var targetContext: TargetTaskProducerContext

    /// Create a phased task producer.
    ///
    /// - phaseStartNode: A virtual node which should be used as an input for all tasks produced by the phase.
    /// - phaseEndNode: A virtual node which should have as inputs all tasks produced by the phase.
    /// Should be package instead of public, but this causes the compiler to generate an unused infinitely recursive thunk.
    public init(_ context: TargetTaskProducerContext, phaseStartNodes: [any PlannedNode], phaseEndNode: any PlannedNode, phaseEndTask: (any PlannedTask)? = nil) {
        self.phaseStartNodes = phaseStartNodes
        self.phaseEndNode = phaseEndNode
        self.phaseEndTask = phaseEndTask ?? context.createPhaseEndTask(inputs: phaseStartNodes, output: phaseEndNode, mustPrecede: [context.targetEndTask])

        self.targetContext = context
        super.init(context)
    }

    var phaseNodeRoot: ConfiguredTarget.GUID {
        return self.context.configuredTarget!.guid
    }

    /// Force a new step in the phase, which will cause all previously tasks to be ordered before this gate.
    ///
    /// If used, the client *must* ensure that `additionalGateTasks` is reported during task generation.
    ///
    /// - Parameters:
    ///   - name: The name to use for the virtual node which will represent completion of the tasks within the current barrier.
    ///   - additionalStartNodes: List of additional `PlannedNode`s the tasks in the phase will take as inputs.
    ///   - additionalMustPrecedeTasks: List of additional `PlannedTask`s the tasks in the phase must precede.  The target end task is always included in this list.
    func addTaskBarrier(name: String, additionalStartNodes: [any PlannedNode], additionalMustPrecedeTasks: [any PlannedTask]) {
        // We implement the barrier by just adjusting the phase start and end nodes we track.

        // Make the new start node the previous phase's end node, and create a new end node.
        self.phaseStartNodes = [self.phaseEndNode] + additionalStartNodes
        self.phaseEndNode = context.createVirtualNode(name)
        self.phaseEndTask = context.createGateTask(phaseStartNodes, output: phaseEndNode, mustPrecede: additionalMustPrecedeTasks + [targetContext.targetEndTask])
        additionalGateTasks.append(phaseEndTask)
    }

    /// Convenience function for appending tasks using from a spec.
    ///
    /// We override this to auto-attach tasks to the phase ordering gates, and to return information from the delegate for reprocessing.
    @discardableResult
    package override func appendGeneratedTasks(_ tasks: inout [any PlannedTask], options: TaskOrderingOptions? = nil, body: (any TaskGenerationDelegate) async -> Void) async -> (tasks: [any PlannedTask], outputs: [FileToBuild]) {
        let (tasks, options, _) = await appendGeneratedTasks(&tasks, usePhasedOrdering: true, options: options, body: body)
        return (tasks, options)
    }

    /// Convenience function for appending tasks using from a spec.
    ///
    /// - Parameters:
    ///   - tasks: The task array to append to.
    ///   - usePhasedOrdering: If true, the tasks are bound between the phase start and end, otherwise they will precede the target end task.
    /// - Returns: The generated tasks and outputs.
    @discardableResult
    func appendGeneratedTasks<T>(_ tasks: inout [any PlannedTask], usePhasedOrdering: Bool, options: TaskOrderingOptions? = nil, body: (any TaskGenerationDelegate) async -> T) async -> (tasks: [any PlannedTask], outputs: [FileToBuild], result: T) {
        let options = options ?? defaultTaskOrderingOptions

        let delegate: PhasedProducerBasedTaskGenerationDelegate
        if usePhasedOrdering {
            delegate = PhasedProducerBasedTaskGenerationDelegate(producer: self, context: context, taskOptions: options, phaseStartNodes: phaseStartNodes, phaseEndTask: phaseEndTask)
        } else {
            delegate = PhasedProducerBasedTaskGenerationDelegate(producer: self, context: context, taskOptions: options, phaseStartNodes: [], phaseEndTask: targetContext.targetEndTask)
        }
        let result = await body(delegate)
        tasks.append(contentsOf: delegate.tasks)
        return (tasks: delegate.tasks, outputs: delegate.outputs, result: result)
    }
}
